package com.trackerapp.service;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Observable;
import java.util.Observer;
import java.util.Queue;

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.location.Location;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.speech.tts.TextToSpeech;
import android.telephony.CellLocation;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.telephony.gsm.GsmCellLocation;
import android.util.Log;

import com.trackerapp.database.CellIDCacheManager;
import com.trackerapp.location.TrackerLocationManager;
import com.trackerapp.model.CellIDLocation;
import com.trackerapp.network.HttpConnection;
import com.trackerapp.util.MessageUtil;

public class LocationService extends Service implements Observer,TextToSpeech.OnInitListener{

    private final String DEBUG_TAG = this.getClass().getSimpleName();
    private final int DEFAULT_TIME_FOR_UPDATE_LOCATION_ACTIVE = 1; // Minutes
    private TelephonyManager telephonyManager;
    private int prevCellId = 0;
    private int currentCellId = 0;
    private double prevLatitude = 0;
    private double prevLongitude = 0;
    private int timerPeriod = DEFAULT_TIME_FOR_UPDATE_LOCATION_ACTIVE;
    private boolean hasMovement = true;
    private int movementCause = 0;
    private boolean newCellIdDetected = false;
    private RecentCellTowers recentCellTowers;
    private static final String UPDATE_LOCATION_TIMER = "UPDATE_LOCATION_TIMER";
    private static final int ALARM_ID = 3001;
    private static final int ALARM_FREQUENCY = 60000; // 1 Minute in Milliseconds
    private final static double AVERAGE_EARTH_RADIUS = 6371000;
    private long counter = 0;
    private Context mContext;
    private Handler mLooperThreadHandler;
    private TextToSpeech tts;
    private String TAG = "LocationService";
    
    
    public void onCreate() {
        super.onCreate();
        counter = 0;
        timerPeriod = DEFAULT_TIME_FOR_UPDATE_LOCATION_ACTIVE;
        hasMovement = true;
        movementCause = 0;
        mContext = this.getApplicationContext();
        TrackerLocationManager.getInstance().addObserver(this);
        recentCellTowers = new RecentCellTowers();
        
        tts = new TextToSpeech(this, this);
       
        new Thread( new Runnable() {
            public void run() {
                    Looper.prepare();
                    mLooperThreadHandler = new Handler();
                    Looper.loop();
            }
        }).start();
        
        
        registerReceiver(mUpdateAlarmReceiver, new IntentFilter(UPDATE_LOCATION_TIMER));
        final Intent updateAlarmIntent = new Intent(UPDATE_LOCATION_TIMER);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(this, ALARM_ID, updateAlarmIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        AlarmManager alarmManager = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
        alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), ALARM_FREQUENCY, pendingIntent);
    }
       
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);
        telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
        int events = PhoneStateListener.LISTEN_CELL_LOCATION;
        telephonyManager.listen(phoneStateListener, events);
        checkForLocationUpdate();
        return START_NOT_STICKY;
    }


    public void onDestroy() {
        super.onDestroy();
        System.out.println(" ############ LocationService onDestroy  ############ ");
        String text = "Service stopped";
        if( tts != null)
        	tts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
    }
    
    @Override
    public void onInit(int status) {

        if (status == TextToSpeech.SUCCESS) {
            int result = tts.setLanguage(Locale.US);
            if (result == TextToSpeech.LANG_MISSING_DATA
                    || result == TextToSpeech.LANG_NOT_SUPPORTED) {
            	System.out.println(" ############ This Language is not supported  ############ ");
            } else {
                String text = "Text to speech setup";
                if( tts != null)
                	tts.speak(text, TextToSpeech.QUEUE_FLUSH, null);                
            }
        } else {
            Log.e("TTS", "Initilization Failed!");
        }
 
    }
    
    private void speakMovementDetected() {
        String text = "Movement Detected";
        if( tts != null)
        	tts.speak(text, TextToSpeech.QUEUE_FLUSH, null);
    }
    
    private final IBinder vbBinder = new LocationBinder();


    //Class for clients to access.
    public class LocationBinder extends Binder implements ILocationService {
        @Override
        public void updateAppForegroundStatus(boolean foreground) {
            //updateForegroundStatus(foreground);
        }
    }


    public IBinder onBind(Intent intent) {
        Log.v(TAG,"onBind()");
        return vbBinder;
    }


    public void onRebind(Intent intent) {
    	Log.v(TAG, "onRebind()");
        super.onRebind(intent);
    }
    
    private void updateCurrentLocation() {
    	TrackerLocationManager.getInstance().requestLocationUpdates(mContext);
    }
    
    @Override
    public void update(Observable observable, Object object) {

    	int status = 0;
    	if( object instanceof Integer) {
    		status = ((Integer) object).intValue();
    	}
    	
    	System.out.println(" ########### Update  ############### ");
    	final int what = status;

    	/* To avoid running in main UI thread post the runnable to worker thread */
        mLooperThreadHandler.post(new Runnable() {

            public void run() {
            	
               if (what == TrackerLocationManager.UPDATE_CURRENT_LOCATION) {

            	    String _latitude = TrackerLocationManager.getInstance().getCurrentLatitude();
                    String _longitude = TrackerLocationManager.getInstance().getCurrentLongitude();
                    //HttpConnection.sendLocationTask(_latitude, _longitude, (int) TrackerLocationManager.getInstance().getCurrentLocation().getAccuracy());
                	
                    if (currentCellId > 0 && prevCellId > 0 && currentCellId != prevCellId &&
                            _latitude != null && _longitude != null) {
                        //Update to cellid cache.
                        CellIDLocation location = new CellIDLocation();
                        location.setCurrentCellId(currentCellId);
                        location.setPreviousCellId(prevCellId);
                        location.setLatitude(Double.parseDouble(_latitude));
                        location.setLongitude(Double.parseDouble(_longitude));
                        location.setAccuracy((int) TrackerLocationManager.getInstance().getCurrentLocation().getAccuracy());
                        try {
                        	System.out.println(" ##########  Update CELL ID Location  ############ "+currentCellId+"  :  "+prevCellId);
                            CellIDCacheManager.getInstance().updateCellIDLocation(location);
                        } catch (Exception e) {
                            e.printStackTrace();
                        } 
                    }
                    
                    if (newCellIdDetected == true) {
                        hasMovement = true;
                    } else if (_latitude != null && _longitude != null && prevLatitude > 0 && prevLongitude > 0) {
                        if (checkForHundredMeterMovement(prevLatitude, prevLongitude, Double.parseDouble(_latitude), Double.parseDouble(_longitude)) == true) {
                            hasMovement = true;
                            System.out.println(" ########### Update 100m Movement TRUE ############### ");
                        } else {
                            hasMovement = false;
                            System.out.println(" ########### Update  100m Movement FALSE ############### ");
                        }
                    } else {
                        // Very first time prevLatitude,prevLongitude would be Null.
                        hasMovement = true;
                    }
                    
                    System.out.println(" ########### Update  ############### has Movement :  "+hasMovement);
                } // End of check what.
            } // End of run() method.
        }); // End of runnable.
    }    
    
    synchronized private void checkForLocationUpdate() {
        CellLocation cellLocation = telephonyManager.getCellLocation();
        newCellIdDetected = false;

        if (cellLocation == null) {
            // Find the current location through wifi.
        	System.out.println(" ##########  Update current location - no cell info, via wifi  ############");
            updateCurrentLocation();
            return;
        }

        if (cellLocation instanceof GsmCellLocation) {

            GsmCellLocation gsmCellLocation = (GsmCellLocation) cellLocation;
            int cellId = gsmCellLocation.getCid();
            System.out.println(" ##########  checkForLocationUpdate CELLID current prev ######### "+cellId+" : "+currentCellId+"  ::  "+prevCellId);
            System.out.println(" ##########  Recent cell Towers ######### "+recentCellTowers.toString());
            if (cellId != currentCellId) {
            	prevCellId = currentCellId;
            	currentCellId = cellId;
            	// Since cell ID fluctuates even when there is no movement, we will need to double check.
            	newCellIdDetected = recentCellTowers.isNew(cellId);
            } else {
            	newCellIdDetected = false;
            }           	
            
            

            // Set previous lat,long.
            String _latitude = TrackerLocationManager.getInstance().getCurrentLatitude();
            String _longitude = TrackerLocationManager.getInstance().getCurrentLongitude();
            
            if (_latitude != null) {
                this.prevLatitude = Double.parseDouble(_latitude);
            } else {
                this.prevLatitude = 0;
            }
            if (_longitude != null) {
                this.prevLongitude = Double.parseDouble(_longitude);
            } else {
                this.prevLongitude = 0;
            }
            
            System.out.println(" ##########  checkForLocationUpdate ######### "+newCellIdDetected+"  Has Movement:  "+hasMovement);
            
            if (hasMovement == true) {
            	System.out.println(" ########## Moving - update current location  ########### ");
                updateCurrentLocation();
            } else if (newCellIdDetected == true) {
            	speakMovementDetected();
                //Check in the cell ID cache for location fix.
                CellIDLocation cacheLocation = null;
                try {
                    cacheLocation = CellIDCacheManager.getInstance().getCellIDLocation(currentCellId, prevCellId);
                } catch (Exception e) {
                    e.printStackTrace();
                } 
                
                if (cacheLocation != null) {
                	System.out.println(" ########## New cell detected - cache found, use as current location  ######### ");
                	Location location = new Location("cache");
                	location.setLatitude(cacheLocation.getLatitude());
                	location.setLatitude(cacheLocation.getLongitude());
                	TrackerLocationManager.getInstance().setCacheLocation(location);
                	hasMovement = true;
                } else {
                	System.out.println("New cell detected - no cache found, update current location");
                    updateCurrentLocation();
                }
            } else {
            	System.out.println("New cell not detected - skip update current location");
            	hasMovement = false;
            }
        }
    }
    
    private boolean checkForHundredMeterMovement(double fromLat, double fromLong, double toLat, double toLong) {
        double distance = calculateAverageDistance(fromLat, fromLong, toLat, toLong);

        if (distance >= 100) {
            return true;
        } else {
            return false;
        }
    }

    public static double calculateAverageDistance(double latitude1,
            double longitude1,
            double latitude2,
            double longitude2) {
		double dLatitude = Math.toRadians(latitude2 - latitude1);
		double dLongitude = Math.toRadians(longitude2 - longitude1);
		double x = Math.sin(dLatitude / 2) * Math.sin(dLatitude / 2) +
		Math.cos(Math.toRadians(latitude1)) * Math.cos(Math.toRadians(latitude2)) *
		Math.sin(dLongitude / 2) * Math.sin(dLongitude / 2);
		
		double angle = 2 * Math.asin(Math.sqrt(x));
		
		return AVERAGE_EARTH_RADIUS * angle;
    }


    
    private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
        @Override
        public void onCellLocationChanged(CellLocation location) {
            super.onCellLocationChanged(location);

            if (location instanceof GsmCellLocation) {
                GsmCellLocation loc = (GsmCellLocation) location;
                recentCellTowers.add(loc.getCid());
            }
        }
    };
    
    private final BroadcastReceiver mUpdateAlarmReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
        	
            counter++;
            if (counter % timerPeriod == 0) {
            	counter = 0;
            	System.out.println( " ########### Alarm trigger 1 minute  ############ ");
                checkForLocationUpdate();
            }
        }
    };

    
    private class RecentCellTowers {
        private final int QUEUE_SIZE = 30;
        private Queue<QueueItem> queue;
        private int cellIdLastAdded = 0;
        private long timeLastAdded = 0;

        private class QueueItem {
            public int cellId;
            public long ts;
        }

        public RecentCellTowers() {
            queue = new LinkedList<QueueItem>();
        }

        public String toString() {
            Iterator<QueueItem> iterator = queue.iterator();
            StringBuffer buf = new StringBuffer();
            long currentTime = System.currentTimeMillis() / 1000;
            while (iterator.hasNext()) {
                QueueItem item = iterator.next();
                buf.append(item.cellId);
                buf.append(':');
                buf.append(currentTime - item.ts);
                if (iterator.hasNext()) {
                    buf.append(", ");
                }
            }
            return buf.toString();
        }

        public boolean isNew(int cellId) {
            //MessageUtil.logMessage("RecentCellTowers.isNew - queue: "+this.toString()+" / is this cell ID: "+String.valueOf(cellId)+" new?", true);
            Iterator<QueueItem> iterator = queue.iterator();
            int count = 0;
            long currentTime = System.currentTimeMillis() / 1000;
            while (iterator.hasNext()) {
                QueueItem item = iterator.next();
                if (((currentTime - item.ts) <= 120) && (item.cellId == cellId)) {
                    count++;
                    if (count > 1) {
                    	return false;
                    }
                }
            }
            return (count <= 1);
        }

        synchronized public void add(int cellId) {
            long currentTime = System.currentTimeMillis() / 1000;
            // sometimes phone state listener reports the same cell ID on change in very short time interval.
            // we will not add such cell ID to the queue.
            if ((cellIdLastAdded == cellId) && ((currentTime - timeLastAdded) <= 3)) {
                return;
            }
            cellIdLastAdded = cellId;
            timeLastAdded = currentTime;
            if (queue.size() >= QUEUE_SIZE) {
                //Pop the first element and add this element.
                queue.poll();
            }
            QueueItem item = new QueueItem();
            item.cellId = cellId;
            item.ts = System.currentTimeMillis() / 1000;
            queue.add(item);
        }
        
    } // RecentCellTowers


}


